// Copyright © 2009-2014 Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE file included with this file.
"use strict";

let EXPORTED_SYMBOLS = [];

let ADDON_ID = "extension-test@dactyl.googlecode.com";

let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
let XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

try {
    // Disable overflow until bug 1021303 is fixed
    var { OverflowableToolbar, CustomizableUIInternal } = Cu.import("resource:///modules/CustomizableUI.jsm");
    OverflowableToolbar.prototype.onOverflow = function (aEvent) {
          if (!this._enabled ||
              (aEvent && aEvent.target != this._toolbar.customizationTarget))
              return;

          let child = this._target.lastChild;

          while (child && this._target.scrollLeftMax > 8) {
              let prevChild = child.previousSibling;

              if (child.getAttribute("overflows") != "false") {
                  this._collapsed.set(child.id, this._target.clientWidth);
                  child.setAttribute("overflowedItem", true);
                  child.setAttribute("cui-anchorid", this._chevron.id);

                  CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target);

                  this._list.insertBefore(child, this._list.firstChild);
                  if (!this._toolbar.hasAttribute("overflowing")) {
                      CustomizableUI.addListener(this);
                  }
                  this._toolbar.setAttribute("overflowing", "true");
              }
              child = prevChild;
          };

          let win = this._target.ownerDocument.defaultView;
          win.UpdateUrlbarSearchSplitterState();
      };
}
catch (e) {}

let locations = {
    foundBrokenIncGC: false,
    iframes: {},
    insecure: {},
    tracking: {},
    file: {},
    scripts: {},
    xmlhttp: {}
};

const DEBUG = true;
function debug() {
    if (DEBUG)
        dump(Array.join(arguments, ", ").replace(/\n?$/, "\n"));
}

function __(key) {
    try {
        return strings.formatStringFromName(key, Array.slice(arguments, 1), arguments.length - 1);
    }
    catch (e) {
        return null;
    }
}
function _() {
    let res = __.apply(this, arguments);
    return res || "[" + Array.map(arguments, String.quote).join(", ") + "]";
}

function module(uri) {
    let object = {};
    Cu.import(uri, object);
    return object;
}

function reportError(e) {
    Cu.reportError(e);
    Services.console.logStringMessage((e.stack || Error().stack).replace(/^/gm, "    "));
}

function wrapCallback(fn, throws) {
    fn.wrapper = function wrapper() {
        try {
            return fn.apply(this, arguments);
        }
        catch (e) {
            Services.console.logStringMessage("Error calling wrapped function " + this + "." + fn.name);
            reportError(e);
            if (throws)
                throw e;
        }
    }
    fn.wrapper.wrapped = fn;
    return fn.wrapper;
}

jsonToDOM.namespaces = {
    html: "http://www.w3.org/1999/xhtml",
    xul: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
};
jsonToDOM.defaultNamespace = jsonToDOM.namespaces.html;
function jsonToDOM(xml, doc, nodes) {
    function parseNamespace(name) {
        var m = /^(?:(.*):)?(.*)$/.exec(name);
        return [jsonToDOM.namespaces[m[1]], m[2]];
    }

    function tag(args, namespace) {
        let [name, attr] = args;
        if ("xmlns" in attr)
            namespace = attr.xmlns;

        if (Array.isArray(name)) {
            var frag = doc.createDocumentFragment();
            Array.forEach(args, function (arg) {
                if (!Array.isArray(arg[0]))
                    frag.appendChild(tag(arg, namespace));
                else
                    arg.forEach(function (arg) {
                        frag.appendChild(tag(arg, namespace));
                    });
            });
            return frag;
        }

        var args = Array.slice(args, 2);
        var vals = parseNamespace(name);
        var elem = doc.createElementNS(vals[0] || namespace,
                                       vals[1]);

        for (var key in attr) {
            var val = attr[key];
            if (nodes && key == "key")
                nodes[val] = elem;

            vals = parseNamespace(key);
            if (typeof val == "function")
                elem.addEventListener(key.replace(/^on/, ""), val, false);
            else if (vals[0])
                elem.setAttributeNS(vals[0], vals[1], val);
            else
                elem.setAttribute(vals[1], val);
        }
        args.forEach(function(e) {
            elem.appendChild(typeof e == "object" ? tag(e, namespace) :
                             e instanceof Node    ? e : doc.createTextNode(e));
        });
        return elem;
    }
    return tag(xml, jsonToDOM.defaultNamespace)
}

function trackNodes(context, selector, callback) {
    let doc = context.ownerDocument || context;

    function watch(event) {
        if (event && event.target != doc)
            return;
        doc.removeEventListener("DOMContentLoaded", watch, false);

        Array.forEach(context.querySelectorAll(selector),
                      callback);
        new doc.defaultView.MutationObserver(function (records) {
            records.forEach(function (record) {
                if (record.addedNodes)
                    Array.filter(record.addedNodes, function (node) node.mozMatchesSelector && node.mozMatchesSelector(selector))
                         .forEach(callback);
            });
        }).observe(doc, {
            childList: true,
            subtree: true
        });
    }

    if (~["interactive", "complete"].indexOf(doc.readyState))
        watch();
    else
        doc.addEventListener("DOMContentLoaded", watch, false);
}

var Services = {
    init: function () {
        this.add("appStartup",          "@mozilla.org/toolkit/app-startup;1",       "nsIAppStartup");
        this.add("blocklist",           "@mozilla.org/extensions/blocklist;1",      "nsITimerCallback")
        this.add("catman",              "@mozilla.org/categorymanager;1",           "nsICategoryManager");
        this.add("clipboardHelper",     "@mozilla.org/widget/clipboardhelper;1",    "nsIClipboardHelper");
        this.add("console",             "@mozilla.org/consoleservice;1",            "nsIConsoleService");
        this.add("dactyl",              "@dactyl.googlecode.com/extra/utils",       "dactylIUtils");
        this.add("directory",           "@mozilla.org/file/directory_service;1",    "nsIProperties");
        this.add("environment",         "@mozilla.org/process/environment;1",       "nsIEnvironment");
        this.add("extensionManager",    "@mozilla.org/extensions/manager;1",        "nsIExtensionManager");
        this.add("memory",              "@mozilla.org/memory-reporter-manager;1",   "nsIMemoryReporterManager");
        this.add("io",                  "@mozilla.org/network/io-service;1",        "nsIIOService");
        this.add("observer",            "@mozilla.org/observer-service;1",          "nsIObserverService");
        this.add("pref",                "@mozilla.org/preferences-service;1",       ["nsIPrefBranch2", "nsIPrefService"]);
        this.add("runtime",             "@mozilla.org/xre/runtime;1",               ["nsIXULAppInfo", "nsIXULRuntime"]);
        this.add("scriptCheck",         "@dactyl.googlecode.com/exttest/eval-check/script");
        this.add("stringBundle",        "@mozilla.org/intl/stringbundle;1",         "nsIStringBundleService");
        this.add("stylesheet",          "@mozilla.org/content/style-sheet-service;1", "nsIStyleSheetService");
        this.add("telemetry",           "@mozilla.org/base/telemetry;1",            "nsITelemetry");
        this.add("thread",              "@mozilla.org/thread-manager;1",            "nsIThreadManager");
        this.add("versionCompare",      "@mozilla.org/xpcom/version-comparator;1",  "nsIVersionComparator");


        this.addClass("Bool",           "@mozilla.org/supports-PRBool;1",           "nsISupportsPRBool", "data");
        this.addClass("ScriptError",    "@mozilla.org/scripterror;1",               "nsIScriptError", "init");
        this.addClass("String",         "@mozilla.org/supports-string;1",           "nsISupportsString", "data");
        this.addClass("Timer",          "@mozilla.org/timer;1",                     "nsITimer", "initWithCallback");
        this.addClass("Xmlhttp",        "@mozilla.org/xmlextras/xmlhttprequest;1",  "nsIXMLHttpRequest");
    },
    reinit: function () {},

    _create: function (classes, ifaces, meth, init, args) {
        try {
            var res = Cc[classes][meth || "getService"]();
            if (ifaces)
                Array.concat(ifaces).forEach(function (iface) res.QueryInterface(Ci[iface]));
            else
                res = res.wrappedJSObject;
        }
        catch (e) {
            return null;
        }

        if (init && args.length) {
            try {
                var isCallable = typeof res[init] === "function";
            }
            catch (e) {} // Ugh.

            if (isCallable)
                res[init].apply(res, args);
            else
                res[init] = args[0];
        }
        return res;
    },

    services: {},
    add: function add(name, class_, ifaces, meth) {
        const self = this;
        this.services[name] = { class: class_, interfaces: Array.concat(ifaces || []) };
        this.__defineGetter__(name, function () {
            delete self[name];
            return self[name] = self._create(class_, ifaces, meth);
        });
    },

    addClass: function (name, class_, ifaces, init) {
        const self = this;
        this[name] = function () self._create(class_, ifaces, "createInstance", init, arguments);
    },

    /**
     * Returns true if the given service is available.
     *
     * @param {string} name The service's cache key.
     */
    has: function (name) name in this.services && this.services[name].class in Cc &&
        this.services[name].interfaces.every(function (iface) iface in Ci)
};
Services.init();

let strings = Services.stringBundle.createBundle("chrome://exttest/locale/exttest.properties");

let WIDTH = 80;
let PREFIX = "    ";
function wrap(items, prefix, sep) {
    sep = sep || " ";
    let width = 0;
    let lines = [];
    lines.__defineGetter__("last", function () this[this.length - 1]);

    for (let [, item] in Iterator(items)) {
        if (!lines.length || item.length > width && lines.last.length > 1) {
            lines.push([prefix]);
            width = WIDTH - prefix.length;
            prefix = PREFIX;
        }
        width -= item.length + sep.length;
        lines.last.push(item, sep);
    }
    lines.last.pop();
    return lines.map(function (l) l.join("")).join("\n").replace(/\s+\n/gm, "\n");
}

function Prefs(branch, defaults) {
    this.constructor = Prefs; // Ends up Object otherwise... Why?

    this.branch = Services.pref[defaults ? "getDefaultBranch" : "getBranch"](branch || "");
    if (this.branch instanceof Ci.nsIPrefBranch2)
        this.branch.QueryInterface(Ci.nsIPrefBranch2);

    this.defaults = defaults ? this : new this.constructor(branch, true);
}
Prefs.prototype = {
    /**
     * Returns a new Prefs object for the sub-branch *branch* of this
     * object.
     *
     * @param {string} branch The sub-branch to return.
     */
    Branch: function Branch(branch) new this.constructor(this.root + branch),

    /**
     * Returns the full name of this object's preference branch.
     */
    get root() this.branch.root,

    /**
     * Returns the value of the preference *name*, or *defaultValue* if
     * the preference does not exist.
     *
     * @param {string} name The name of the preference to return.
     * @param {*} defaultValue The value to return if the preference has no value.
     * @optional
     */
    get: function get(name, defaultValue) {
        let type = this.branch.getPrefType(name);

        if (type === Ci.nsIPrefBranch.PREF_STRING)
            return this.branch.getComplexValue(name, Ci.nsISupportsString).data;

        if (type === Ci.nsIPrefBranch.PREF_INT)
            return this.branch.getIntPref(name);

        if (type === Ci.nsIPrefBranch.PREF_BOOL)
            return this.branch.getBoolPref(name);

        return defaultValue;
    },

    /**
     * Returns true if the given preference exists in this branch.
     *
     * @param {string} name The name of the preference to check.
     */
    has: function has(name) this.branch.getPrefType(name) !== 0,

    /**
     * Returns an array of all preference names in this branch or the
     * given sub-branch.
     *
     * @param {string} branch The sub-branch for which to return preferences.
     * @optional
     */
    getNames: function getNames(branch) this.branch.getChildList(branch || "", { value: 0 }),

    /**
     * Returns true if the given preference is set to its default value.
     *
     * @param {string} name The name of the preference to check.
     */
    isDefault: function isDefault(name) !this.branch.prefHasUserValue(name),

    /**
     * Sets the preference *name* to *value*. If the preference already
     * exists, it must have the same type as the given value.
     *
     * @param {name} name The name of the preference to change.
     * @param {string|number|boolean} value The value to set.
     */
    set: function set(name, value) {
        let type = typeof value;
        if (type === "string")
            this.branch.setComplexValue(name, Ci.nsISupportsString, Services.String(value));
        else if (type === "number")
            this.branch.setIntPref(name, value);
        else if (type === "boolean")
            this.branch.setBoolPref(name, value);
        else
            throw TypeError("Unknown preference type: " + type);
    },

    /**
     * Resets the preference *name* to its default value.
     *
     * @param {string} name The name of the preference to reset.
     */
    reset: function reset(name) {
        if (this.branch.prefHasUserValue(name))
            this.branch.clearUserPref(name);
    }
};

let allPrefs = new Prefs("");
let prefs = new Prefs("extensions.exttest.");
prefs.console = prefs.Branch("console.");
prefs.safe = prefs.Branch("safe.");
prefs.status = prefs.Branch("status.");

function confirmQuit() {
    var veto = Services.Bool(false);
    Services.observer.notifyObservers(veto, "quit-application-requested", null);
    return !veto.data;
}

function quitApplication(restart) {
    if (!confirmQuit())
        return;

    Services.appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | (restart && Ci.nsIAppStartup.eRestart));
}

// Categories which create globals
let categories = Array.concat.apply(null,
    ["global-constructor",
     "global-constructor-prototype-alias",
     "global-property",
     "global-privileged-property",
     "global-static-nameset",
     "global-dynamic-nameset",
     "DOM-class",
     "DOM-interface"]
     .map(function (cat) "JavaScript-" + cat) // Prevent spurious validator warnings
     .map(function (cat) [cat, cat.replace(/-/g, " ")]));


function fixURL(url) url && String(url).replace(/.* -> /, "");

function isLocalProtocol(uri) {
    let proto = Services.io.getProtocolHandler(uri.scheme);
    return proto.protocolFlags & proto.URI_IS_LOCAL_RESOURCE;
}
function isUIProtocol(uri) {
    let proto = Services.io.getProtocolHandler(uri.scheme);
    return (proto.protocolFlags & proto.URI_IS_UI_RESOURCE) || uri.scheme == "about";
}

function isString(val) ({}).toString.call(val) == "[object String]";

function iterCategory(category) {
    let enumerator = Services.catman.enumerateCategory(category);
    while (enumerator.hasMoreElements())
        yield enumerator.getNext().QueryInterface(Ci.nsISupportsCString).data;
}

function iter(obj, iface) {
    if (arguments.length == 2 && iface instanceof Ci.nsIJSIID)
        return (function () {
            for (let o in iter(obj))
                yield o.QueryInterface(iface);
        })();

    let args = arguments;
    let res = Iterator(obj);

    if (~["[object Iterator]", "[object Generator]"].indexOf({}.toString.call(obj)))
        ;
    else if (obj instanceof Ci.nsIDOMHTMLCollection ||
             obj instanceof Ci.nsIDOMNodeList)
        res = array.iterItems(obj);
    else if (Ci.nsIDOMNamedNodeMap && obj instanceof Ci.nsIDOMNamedNodeMap ||
             Ci.nsIDOMMozNamedAttrMap && obj instanceof Ci.nsIDOMMozNamedAttrMap)
        res = (function () {
            for (let i = 0; i < obj.length; i++)
                yield [obj.name, obj];
        })();
    else if (obj instanceof Ci.mozIStorageStatement)
        res = (function (obj) {
            while (obj.executeStep())
                yield obj.row;
            obj.reset();
        })(obj);
    else if ("getNext" in obj) {
        if ("hasMoreElements" in obj)
            res = (function () {
                while (obj.hasMoreElements())
                    yield obj.getNext();
            })();
        else if ("hasMore" in obj)
            res = (function () {
                while (obj.hasMore())
                    yield obj.getNext();
            })();
    }
    else if ("enumerator" in obj) {
        if (callable(obj.enumerator))
            return iter(obj.enumerator());
        return iter(obj.enumerator);
    }
    return res;
}

function log(message, items, frame, error) {
    if (frame)
        Services.console.logMessage(
            Services.ScriptError(message + (items ? ": " + items.join(", ") : ""),
                                 fixURL(frame.filename),
                                 null,
                                 frame.lineNumber,
                                 null,
                                 Ci.nsIScriptError[error ? "errorFlag" : "warningFlag"],
                                 "chrome javascript exttest Extension Test"));
    else
        Services.console.logStringMessage(message + (items ? ": " + items.join(", ") : ""));

    if (frame && frame.lineNumber)
        message = fixURL(frame.filename) + ":" + frame.lineNumber + ": " + message
    dump(message + (items ? ":\n\t" + items.join("\n\t") : "") + "\n")
}

function delay(callback) {
    Services.thread.mainThread
            .dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
}

const BLOCKLIST_PREF = "extensions.blocklist.url";
function refreshBlocklist() {
    Services.blocklist.notify(null);
}

function minimizeMemory(callback) {
    /* From aboutMemory.js: */
    // For maximum effect, this returns to the event loop between each
    // notification.  See bug 610166 comment 12 for an explanation.
    // Ideally a single notification would be enough.

    function notify() {
        Services.observer.notifyObservers(null, "memory-pressure", "heap-minimize");

        if (++j >= 3)
            next = callback;
        if (next)
            delay(next);
    }

    var j = 0;
    let next = notify;
    notify();
}


// Need telemetry recording enabled in order to monitor slow SQL
// statements.
Services.telemetry.canRecord = true;
function getSlowSQLTime() {
    if (!Services.telemetry.slowSQL)
        return 0;

    return [time
            for ([key, [count, time]] in Iterator(Services.telemetry.slowSQL.mainThread))
            if (/^Untracked SQL|places\.sqlite/.test(key))]
        .reduce((a, b) => a + b, 0);
}

let wantType;
function refreshCompartments() {
    compartments.length = 0;

    let num = {};
    function collect(path, process) {
        let name = path.replace(/^compartments\//, "")
                       .replace(/\\/g, "/")
                 + " [pid=" + (process || "Main") + "] ";

        num[name] = (num[name] || 0) + 1;
        compartments.push(name + "(" + num[name] + ")");
    }

    if (Services.memory.enumerateReporters)
        for (let r in iter(Services.memory.enumerateReporters(), Ci.nsIMemoryReporter))
            if (/^compartments\//.test(r.path))
                collect(r.path);

    if (Services.memory.enumerateMultiReporters)
        for (let r in iter(Services.memory.enumerateMultiReporters(), Ci.nsIMemoryMultiReporter))
            if (r.name == "compartments")
                r.collectReports(function (process, path, kind, units, amount, desc) {
                    collect(path, process);
                }, null);

    timer.cancel();
    timer.initWithCallback(refreshCompartments,
                           prefs.get("compartment-timeout", 15000),
                           Services.Timer.TYPE_ONE_SHOT);
}

if (Services.has("memory")) {
    var compartments = [];
    var timer = Services.Timer(refreshCompartments, 5000,
                               Services.Timer.TYPE_ONE_SHOT);
}

function newValuesMsg(name, key) {
    let base = "message." + (key || "new-values");

    if (/(.*?)\.(.*)/.exec(name))
        var res = __(base + "." + RegExp.$1, RegExp.$2);

    return res || __(base + "." + name)
               || __(base, name);
}

if (typeof XPCSafeJSObjectWrapper == "undefined")
    var XPCSafeJSObjectWrapper = XPCNativeWrapper;

function xpath(path, context) {
    let result = (context.ownerDocument || context).evaluate(path, context, null, Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    let res = {
        get resultType() result.resultType,
        get snapshotLength() result.snapshotLength,
        snapshotItem: function (i) result.snapshotItem(i),
        __iterator__: function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
    };
    return res;
}

let { hasOwnProperty } = {};
